[情境劇場]
解師傅:本來明天要請周J倫來表演,但他臨時有事,所以改請六月天樂團了!
小當家:哦!!那我要改一下了
解師傅:改什麼?
小當家:我的腦袋記錄名單裡,周J倫愛吃雞蛋豆腐,六月天裡面的團員有兩個不愛,因為現在變更名單,所以明天上的菜色將會做更動
解師傅:真有你的...
useMemo 是一個 Memorized Hook,每當 render 時都會重新渲染(re-render
)組件,如果遇到不必要渲染程式,重新渲染就會造成效能上的浪費,透過 useMemo 可以將函式運算完的值存一個記憶體(memoization),你也可以把它視為緩存值,以減少組件不必要的重新渲染
import { useMemo } from "react";
const memoizedValue = useMemo(() => { return fn(a) }, [a]);
第一個參數為函式,函式回傳不必要重新渲染的程式
第二個參數為 dependencies 陣列,useMemo 會依 dependencies 陣列去做比對差異,如果 dependencies 有變動,才會重新渲染,並回傳一個值
如沒有 dependencies,則放空陣列,每次 render 時都會計算新的值。
複雜的計算本身就會吃比較多效能,每次狀態有變動又 re-render,但事實上我們只需要加上 dependencies
就可以減少 re-render 的次數
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(100);
const changeCount = (e) => {
setCount(value);
};
const evenNumbers = [];
for (let i = 1; i <= count; i++) {
if (i % 2 === 0 && String(i).includes("2")) {
evenNumbers.push(i);
}
}
return (
<div>
<h2>{count}</h2>
<div>
<input type="number" value={count} onChange={(e) => changeCount(e)} />
</div>
<div>1~{count} 為偶數且數字有 2 的號碼:</div>
{evenNumbers.join(", ")}
</div>
);
}
可以不用太仔細讀程式碼,這邊簡單說明一下程式範例目的:
打開 codesandbox 程式碼範例 看看!
這邊乍看之下沒什麼太大的問題,於是再加入了一個計時器進來
import { useState, useEffect } from "react";
export default function App() {
const [count, setCount] = useState(50);
const changeCount = (e) => {
setCount(value);
};
const evenNumbers = [];
for (let i = 1; i <= count; i++) {
if (i % 2 === 0 && String(i).includes("2")) {
evenNumbers.push(i);
}
console.log("render evenNumbers");
}
const time = useTime();
function useTime() {
const [time, setTime] = useState(0);
useEffect(() => {
let i = 0;
const interval = setInterval(() => {
setTime(i++);
console.log("render time");
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
return time;
}
return (
<div>
<span>time:{time}</span>
<h2>{count}</h2>
<div>
<input type="number" value={count} onChange={(e) => changeCount(e)} />
</div>
<div>1~{count} 為偶數且數字有 2 的號碼:</div>
{evenNumbers.join(", ")}
</div>
);
}
加了計時器後,每 1 秒計時器會 +1
為了方便觀察,我們在計時器和 evenNumbers 的函式分別下了 console.log
,每當計時器變動,會發現 evenNumbers 的地方又重新渲染了一次!
由此可知,只要有元素狀態變更,整個都會重新 render 一次,這造成了效能上的浪費
打開 codesandbox 程式碼範例 看看
這時候就可以用 useMemo
解救這個問題!
const evenNumbers = useMemo(() => {
const result = [];
for (let i = 1; i <= count; i++) {
if (i % 2 === 0 && String(i).includes("2")) {
result.push(i);
}
console.log("evenNumbers");
}
return result;
}, [count]);
我們用了 useMemo ,讓 evenNumbers 只在 count 有變動時才會渲染,現在可以看到,即使計時器一直變動,evenNumbers 也不會再重新 render 囉!
打開 codesandbox 程式碼範例 看看
在 JavaScript 用相同的物件或陣列做比對時,你會發現
{} === {} // false
[] === [] // false
因為 object
跟 Array
非基本型態,比較都是 by reference,雖然內容一樣,但實際上是不一樣的 object、Array,所以如果我們把 object、Array 當作 dependencies
,還是會每次都再重新渲染一次,這樣是沒有意義的
import { useEffect, useState } from "react";
function App() {
const [state, setState] = useState(true);
const [inputValue, setInputValue] = useState("");
const style = {
backgroundColor: state ? "black" : "yellow",
width: "100px",
height: "100px",
margin: "auto"
};
useEffect(() => {
console.log("Change Color");
}, [style]);
return (
<div>
<div style={style}></div>
<input
value={inputValue}
onChange={(e) => {
setInputValue(e.target.value);
}}
placeholder="輸入文字"
/>
<button
onClick={() => {
setState((state) => !state);
}}
>
Change Color
</button>
</div>
);
}
export default App;
簡單說明一下情境:
打開 codesandbox 程式碼範例 看看
目前只有一開始執行的 useEffect,看起來沒什麼大問題
試著輸入文字將會發現
還是執行了 side effect!!
這就是因為物件或陣列即使做了比對,但因為是 by reference
,所以還是會被認定為不一樣,進而呼叫 side effect
我們只要用 useMemo
包起來,就可以解決這個問題
const style = useMemo(() => {
return {
backgroundColor: state ? "black" : "yellow",
width: "100px",
height: "100px",
margin: "auto"
};
}, [state]);
現在即使輸入了文字,也不會再執行 side effect 囉!
打開 codesandbox 程式碼範例 看看吧!
其實遇到這種情況,eslint 也會跳出建議訊息,防止開發者沒注意到這個問題
The 'style' object makes the dependencies of useEffect Hook (at line 17) change on every render. To fix this, wrap the initialization of 'style' in its own useMemo() Hook. (react-hooks/exhaustive-deps)eslint
useMemo
替我們節省了很多效能,但也不要濫用 useMemo
,過多的記憶體,也是會造成效能上的問題,所以我們要用在適當的情境使用,如會頻繁的渲染就較不適合,最好是用在執行速度很慢、變動性不大的函式,以減少重新渲染的目的。
Understanding useMemo and useCallback
How To Use Memoization To Drastically Increase React Performance
本文將同步更新至我的部落格
Lala 的前端大補帖